package com.googlecode.cswish.struts;

import java.io.IOException;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.struts2.ServletActionContext;

import com.googlecode.cswish.model.FreemarkerPageContext;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.struts.conversion.PageElementService;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.util.FrameConstant;
import com.opensymphony.module.sitemesh.Config;
import com.opensymphony.module.sitemesh.Factory;
import com.opensymphony.sitemesh.Content;
import com.opensymphony.sitemesh.ContentProcessor;
import com.opensymphony.sitemesh.Decorator;
import com.opensymphony.sitemesh.DecoratorSelector;
import com.opensymphony.sitemesh.compatability.DecoratorMapper2DecoratorSelector;
import com.opensymphony.sitemesh.compatability.PageParser2ContentProcessor;
import com.opensymphony.sitemesh.webapp.ContainerTweaks;
import com.opensymphony.sitemesh.webapp.ContentBufferingResponse;
import com.opensymphony.sitemesh.webapp.SiteMeshWebAppContext;
import com.opensymphony.sitemesh.webapp.decorator.NoDecorator;
import com.opensymphony.xwork2.ActionContext;

/**
 * Core Filter for integrating SiteMesh into a Java web application.
 *
 * @author Joe Walnes
 * @author Scott Farquhar
 * @since SiteMesh 3
 */
public class SiteMeshFilter implements Filter {

    private FilterConfig filterConfig;
    private ContainerTweaks containerTweaks;
    private static final String ALREADY_APPLIED_KEY = "com.opensymphony.sitemesh.APPLIED_ONCE";
    
    private boolean enableContentWrapper = true;
    
    private PageElementService pageElementService;

    public PageElementService getPageElementService() {
    	if (pageElementService == null) {
    		pageElementService = ObjectFactory.getInstance().getBean(PageElementService.class, false);
    	}
    	return pageElementService;
    }
    
    public void init(FilterConfig filterConfig) {
        this.filterConfig = filterConfig;
        containerTweaks = new ContainerTweaks();
        if ("false".equals(System.getProperty("sitemesh.contentWrapper"))) {
        	enableContentWrapper = false;
        }
    }

    public void destroy() {
        filterConfig = null;
        containerTweaks = null;
    }

    /**
     * Main method of the Filter.
     * <p>Checks if the Filter has been applied this request. If not, parses the page
     * and applies {@link com.opensymphony.module.sitemesh.Decorator} (if found).
     */
    public void doFilter(ServletRequest rq, ServletResponse rs, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) rq;
        HttpServletResponse response = (HttpServletResponse) rs;
        ServletContext servletContext = filterConfig.getServletContext();

        SiteMeshWebAppContext webAppContext = new SiteMeshWebAppContext(request, response, servletContext);

        ContentProcessor contentProcessor = initContentProcessor(webAppContext);
        DecoratorSelector decoratorSelector = initDecoratorSelector(webAppContext);

        if (filterAlreadyAppliedForRequest(request)) {
        	ActionContext ctx = ServletActionContext.getActionContext(request);
        	
        	if (ctx != null) {
	        	PageElements ps = (PageElements)ctx.getValueStack().getContext().get(FrameConstant.STACK_PAGEELEMENTS);
	        	
	        	String path = requestUrlToTemplatePath(request);
	        	if (path.endsWith(".dec")) {
		        	PageElements tplPs = getPageElementService().getTemplatePageElements(ctx.getActionInvocation(), path);
		        	Map<String, Object> context = ctx.getValueStack().getContext();
		        	
		        	// FreemarkerDecoratorServlet.preTemplateProcess
		        	// merge template pageElements with page pageElements, template pageElements has the higher priority
		        	FreemarkerPageContext tplCtx = tplPs.safeContext();
		        	FreemarkerPageContext pageCtx = ps.getContext();
		        	boolean isTopPage = !"/decorators/self.dec".equals(path);			// TODO: better solution?
					FreemarkerPageContext mergeCtx = tplCtx.merge(pageCtx, isTopPage);
		        	mergeCtx.beginRequest();
		        	
		        	
		        	try {
			        	tplPs.setContext(mergeCtx);
			        	context.put(FrameConstant.STACK_PAGEELEMENTS, tplPs);
			        	
			        	// Prior to Servlet 2.4 spec, it was unspecified whether the filter should be called again upon an include().
			            chain.doFilter(request, response);
		        	} finally {
		        		tplPs.setContext(tplCtx);
		        	}
	        	} else {
	        		chain.doFilter(request, response);
	        	}
        	} else {
        		chain.doFilter(request, response);
        	}
            return;
        }

        if (!contentProcessor.handles(webAppContext)) {
            // Optimization: If the content doesn't need to be processed, bypass SiteMesh.
            chain.doFilter(request, response);
            return;
        }

        if (containerTweaks.shouldAutoCreateSession()) {
            // Some containers (such as Tomcat 4) will not allow sessions to be created in the decorator.
            // (i.e after the response has been committed).
            request.getSession(true);
        }

        try {

            Content content = obtainContent(contentProcessor, webAppContext, request, response, chain);

            if (content == null) {
                return;
            }

            Decorator decorator = decoratorSelector.selectDecorator(content, webAppContext);
            if (decorator instanceof NoDecorator) {
            	content = wrapper(request, content);
            } else {
            	// merge the PageContext to template PageContext, see method 'doFilter'
            }
            decorator.render(content, webAppContext);
        } catch (IllegalStateException e) {
            // Some containers (such as WebLogic) throw an IllegalStateException when an error page is served.
            // It may be ok to ignore this. However, for safety it is propegated if possible.
            if (!containerTweaks.shouldIgnoreIllegalStateExceptionOnErrorPage()) {
                throw e;
            }
        } catch (RuntimeException e) {
            if (containerTweaks.shouldLogUnhandledExceptions()) {
                // Some containers (such as Tomcat 4) swallow RuntimeExceptions in filters.
                servletContext.log("Unhandled exception occurred whilst decorating page", e);
            }
            throw e;
        } catch (ServletException e) {
            request.setAttribute(ALREADY_APPLIED_KEY, null);
            throw e;
        }

    }

	private Content wrapper(HttpServletRequest request, Content content) {
		if (enableContentWrapper) {
			ActionContext ctx = ServletActionContext.getActionContext(request);
			if (ctx != null) {
				PageElements ps = (PageElements) ctx.getValueStack()
						.getContext().get(FrameConstant.STACK_PAGEELEMENTS);
				FreemarkerPageContext fmPageContext = ps.getContext();
				if (fmPageContext != null && !fmPageContext.isProcessed()) {
					String resultCode = request
							.getParameter(FrameConstant.PARAM_RESULTCODE);
					if (StringUtils.isEmpty(resultCode)) {
						// add header & footer for all the normal page
						content = new SiteMeshContentWrapper(content,
								fmPageContext.buildHeader(),
								fmPageContext.buildFooter());
					}
				}
			}
		}
		return content;
	}

	protected ContentProcessor initContentProcessor(SiteMeshWebAppContext webAppContext) {
        // TODO: Remove heavy coupling on horrible SM2 Factory
        Factory factory = Factory.getInstance(new Config(filterConfig));
        factory.refresh();
        return new PageParser2ContentProcessor(factory);
    }

    protected DecoratorSelector initDecoratorSelector(SiteMeshWebAppContext webAppContext) {
        // TODO: Remove heavy coupling on horrible SM2 Factory
        Factory factory = Factory.getInstance(new Config(filterConfig));
        factory.refresh();
        return new DecoratorMapper2DecoratorSelector(factory.getDecoratorMapper());
    }

    /**
     * Continue in filter-chain, writing all content to buffer and parsing
     * into returned {@link com.opensymphony.module.sitemesh.Page} object. If
     * {@link com.opensymphony.module.sitemesh.Page} is not parseable, null is returned.
     */
    private Content obtainContent(ContentProcessor contentProcessor, SiteMeshWebAppContext webAppContext,
                                  HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        ContentBufferingResponse contentBufferingResponse = new ContentBufferingResponse(response, contentProcessor, webAppContext);
        chain.doFilter(request, contentBufferingResponse);
        // TODO: check if another servlet or filter put a page object in the request
        //            Content result = request.getAttribute(PAGE);
        //            if (result == null) {
        //                // parse the page
        //                result = pageResponse.getPage();
        //            }
        webAppContext.setUsingStream(contentBufferingResponse.isUsingStream());
        return contentBufferingResponse.getContent();
    }

    private boolean filterAlreadyAppliedForRequest(HttpServletRequest request) {
        if (request.getAttribute(ALREADY_APPLIED_KEY) == Boolean.TRUE) {
            return true;
        } else {
            request.setAttribute(ALREADY_APPLIED_KEY, Boolean.TRUE);
            return false;
        }
    }
    
    /**
     * Maps the request URL to a template path that is passed to 
     * {@link Configuration#getTemplate(String, Locale)}. You can override it
     * (i.e. to provide advanced rewriting capabilities), but you are strongly
     * encouraged to call the overridden method first, then only modify its
     * return value. 
     * @param request the currently processed request
     * @return a String representing the template path
     */
    protected String requestUrlToTemplatePath(HttpServletRequest request)
    {
        // First, see if it is an included request
        String includeServletPath  = (String) request.getAttribute("javax.servlet.include.servlet_path");
        if(includeServletPath != null)
        {
            // Try path info; only if that's null (servlet is mapped to an
            // URL extension instead of to prefix) use servlet path.
            String includePathInfo = (String) request.getAttribute("javax.servlet.include.path_info");
            return includePathInfo == null ? includeServletPath : includePathInfo;
        } 
        // Seems that the servlet was not called as the result of a 
        // RequestDispatcher.include(...). Try pathInfo then servletPath again,
        // only now directly on the request object:
        String path = request.getPathInfo();
        if (path != null) return path;
        path = request.getServletPath();
        if (path != null) return path;
        // Seems that it is a servlet mapped with prefix, and there was no extra path info.
        return "";
    }

}
